"use strict";

(function(root, factory) {
    /* global jQuery */
    if(typeof define === 'function' && define.amd) {
        define(function(require, exports, module) {
            root.nativePointerMock = module.exports = factory(require("jquery"));
        });
    } else {
        root.nativePointerMock = factory(jQuery);
    }
}(window, function($) {
    var UA = (function() {
        var ua = window.navigator.userAgent,
            matches,
            version,
            result = { userAgent: ua };

        if(/i(pod|pad|phone)/ig.test(ua)) {
            matches = ua.match(/os (\d+)_(\d+)_?(\d+)?/i);
            version = matches ? [parseInt(matches[1], 10), parseInt(matches[2], 10), parseInt(matches[3] || 0, 10)] : [];
            result.ios = version[0];
        }

        if(/android|htc_|silk/ig.test(ua)) {
            matches = ua.match(/android (\d+)\.(\d+)\.?(\d+)?/i);
            version = matches ? [parseInt(matches[1], 10), parseInt(matches[2], 10), parseInt(matches[3] || 0, 10)] : [];
            result.android = version[0];
        }

        if(/Chrome/ig.test(ua)) {
            matches = ua.match(/Chrome\/(\d+)\.(\d+)\.?(\d+)?/i);
            version = [parseInt(matches[1], 10), parseInt(matches[2], 10), parseInt(matches[3] || 0, 10)];
            result.chrome = version[0];
        }

        return result;
    })();

    var simulateEvent = (function() {

        var isString = function(val) { return typeof val === "string"; },
            isBoolean = function(val) { return typeof val === "boolean"; },
            isObject = function(val) { return typeof val === "object"; },

            MOUSE_EVENTS = {
                "click": 1,
                "dblclick": 1,
                "mouseover": 1,
                "mouseout": 1,
                "mousedown": 1,
                "mouseup": 1,
                "mousemove": 1,
                "contextmenu": 1
            },

            MS_POINTER_EVENTS = {
                MSPointerOver: 1,
                MSPointerOut: 1,
                MSPointerDown: 1,
                MSPointerUp: 1,
                MSPointerMove: 1
            },

            POINTER_EVENTS = {
                "pointerover": 1,
                "pointerOut": 1,
                "pointerdown": 1,
                "pointerup": 1,
                "pointermove": 1
            },

            KEY_EVENTS = {
                "keydown": 1,
                "keyup": 1,
                "keypress": 1
            },

            UI_EVENTS = {
                submit: 1,
                blur: 1,
                change: 1,
                focus: 1,
                resize: 1,
                scroll: 1,
                select: 1
            },

            TOUCH_EVENTS = {
                "touchstart": 1,
                "touchmove": 1,
                "touchend": 1,
                "touchcancel": 1
            },

            GESTURE_EVENTS = {
                "gesturestart": 1,
                "gesturechange": 1,
                "gestureend": 1
            },

            BUBBLE_EVENTS = $.extend({
                scroll: 1,
                resize: 1,
                reset: 1,
                submit: 1,
                change: 1,
                select: 1,
                error: 1,
                abort: 1
            }, MOUSE_EVENTS, KEY_EVENTS, TOUCH_EVENTS);



        var simulateKeyEvent = function(target, type, bubbles, cancelable, view, ctrlKey, altKey, shiftKey, metaKey, keyCode, charCode) {
            if(!target) {
                throw Error("Invalid target");
            }

            if(isString(type)) {
                type = type.toLowerCase();
                switch(type) {
                    case "textevent":
                        type = "keypress";
                        break;
                    case "keyup":
                    case "keydown":
                    case "keypress":
                        break;
                    default:
                        throw Error("Event type '" + type + "' not supported.");
                }
            } else {
                throw Error("Event type must be a string.");
            }

            if(!isBoolean(bubbles)) {
                bubbles = true;
            }

            if(!isBoolean(cancelable)) {
                cancelable = true;
            }

            if(!isObject(view)) {
                view = window;
            }

            if(!isBoolean(ctrlKey)) {
                ctrlKey = false;
            }

            if(!isBoolean(altKey)) {
                altKey = false;
            }

            if(!isBoolean(shiftKey)) {
                shiftKey = false;
            }

            if(!isBoolean(metaKey)) {
                metaKey = false;
            }

            if(!$.isNumeric(keyCode)) {
                keyCode = 0;
            }

            if(!$.isNumeric(charCode)) {
                charCode = 0;
            }

            var customEvent = null;

            if($.isFunction(document.createEvent)) {

                try {
                    customEvent = document.createEvent("KeyEvents");

                    customEvent.initKeyEvent(type, bubbles, cancelable, view, ctrlKey,
                        altKey, shiftKey, metaKey, keyCode, charCode);

                } catch(ex) {

                    try {

                        customEvent = document.createEvent("Events");

                    } catch(uiError) {

                        customEvent = document.createEvent("UIEvents");

                    } finally {

                        customEvent.initEvent(type, bubbles, cancelable);

                        customEvent.view = view;
                        customEvent.altKey = altKey;
                        customEvent.ctrlKey = ctrlKey;
                        customEvent.shiftKey = shiftKey;
                        customEvent.metaKey = metaKey;
                        customEvent.keyCode = keyCode;
                        customEvent.charCode = charCode;
                    }

                }

                target.dispatchEvent(customEvent);

            } else if(isObject(document.createEventObject)) {

                customEvent = document.createEventObject();

                customEvent.bubbles = bubbles;
                customEvent.cancelable = cancelable;
                customEvent.view = view;
                customEvent.ctrlKey = ctrlKey;
                customEvent.altKey = altKey;
                customEvent.shiftKey = shiftKey;
                customEvent.metaKey = metaKey;

                customEvent.keyCode = (charCode > 0) ? charCode : keyCode;

                target.fireEvent("on" + type, customEvent);

            } else {
                throw Error("No event simulation supported");
            }
        };

        var simulateMouseEvent = function(target, type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget) {
            if(!target) {
                throw Error("Invalid target.");
            }


            if(isString(type)) {
                if(!MOUSE_EVENTS[type.toLowerCase()] && !MS_POINTER_EVENTS[type] && !POINTER_EVENTS[type]) {
                    throw Error("Event type '" + type + "' not supported.");
                }
            } else {
                throw Error("Event type must be a string.");
            }

            if(!isBoolean(bubbles)) {
                bubbles = true;
            }

            if(!isBoolean(cancelable)) {
                cancelable = (type !== "mousemove");
            }

            if(!isObject(view)) {
                view = window;
            }

            if(!$.isNumeric(detail)) {
                detail = 1;
            }

            if(!$.isNumeric(screenX)) {
                screenX = 0;
            }

            if(!$.isNumeric(screenY)) {
                screenY = 0;
            }

            if(!$.isNumeric(clientX)) {
                clientX = 0;
            }

            if(!$.isNumeric(clientY)) {
                clientY = 0;
            }

            if(!isBoolean(ctrlKey)) {
                ctrlKey = false;
            }

            if(!isBoolean(altKey)) {
                altKey = false;
            }

            if(!isBoolean(shiftKey)) {
                shiftKey = false;
            }

            if(!isBoolean(metaKey)) {
                metaKey = false;
            }

            if(!$.isNumeric(button)) {
                button = 0;
            }

            relatedTarget = relatedTarget || null;

            var customEvent = null;

            if($.isFunction(document.createEvent)) {

                customEvent = document.createEvent("MouseEvents");

                if(customEvent.initMouseEvent) {
                    customEvent.initMouseEvent(type, bubbles, cancelable, view, detail,
                        screenX, screenY, clientX, clientY,
                        ctrlKey, altKey, shiftKey, metaKey,
                        button, relatedTarget);
                } else {
                    customEvent = document.createEvent("UIEvents");
                    customEvent.initEvent(type, bubbles, cancelable);
                    customEvent.view = view;
                    customEvent.detail = detail;
                    customEvent.screenX = screenX;
                    customEvent.screenY = screenY;
                    customEvent.clientX = clientX;
                    customEvent.clientY = clientY;
                    customEvent.ctrlKey = ctrlKey;
                    customEvent.altKey = altKey;
                    customEvent.metaKey = metaKey;
                    customEvent.shiftKey = shiftKey;
                    customEvent.button = button;
                    customEvent.relatedTarget = relatedTarget;
                }

                if(relatedTarget && !customEvent.relatedTarget) {
                    if(type === "mouseout") {
                        customEvent.toElement = relatedTarget;
                    } else if(type === "mouseover") {
                        customEvent.fromElement = relatedTarget;
                    }
                }

                target.dispatchEvent(customEvent);

            } else if(isObject(document.createEventObject)) { //IE

                customEvent = document.createEventObject();

                customEvent.bubbles = bubbles;
                customEvent.cancelable = cancelable;
                customEvent.view = view;
                customEvent.detail = detail;
                customEvent.screenX = screenX;
                customEvent.screenY = screenY;
                customEvent.clientX = clientX;
                customEvent.clientY = clientY;
                customEvent.ctrlKey = ctrlKey;
                customEvent.altKey = altKey;
                customEvent.metaKey = metaKey;
                customEvent.shiftKey = shiftKey;

                switch(button) {
                    case 0:
                        customEvent.button = 1;
                        break;
                    case 1:
                        customEvent.button = 4;
                        break;
                    case 2:
                        break;
                    default:
                        customEvent.button = 0;
                }

                customEvent.relatedTarget = relatedTarget;

                target.fireEvent("on" + type, customEvent);

            } else {
                throw Error("No event simulation framework present.");
            }
        };

        var simulateUIEvent = function(target, type, bubbles, cancelable, view, detail) {
            if(!target) {
                throw Error("Invalid target.");
            }

            if(isString(type)) {
                type = type.toLowerCase();

                if(!UI_EVENTS[type]) {
                    throw Error("Event type '" + type + "' not supported.");
                }
            } else {
                throw Error("Event type must be a string.");
            }

            var customEvent = null;

            if(!isBoolean(bubbles)) {
                bubbles = (type in BUBBLE_EVENTS);

                if(!isBoolean(cancelable)) {
                    cancelable = (type === "submit");

                    if(!isObject(view)) {
                        view = window;

                        if(!$.isNumeric(detail)) {
                            detail = 1;

                            if($.isFunction(document.createEvent)) {

                                customEvent = document.createEvent("UIEvents");
                                customEvent.initUIEvent(type, bubbles, cancelable, view, detail);

                                target.dispatchEvent(customEvent);

                            } else if(isObject(document.createEventObject)) {

                                customEvent = document.createEventObject();

                                customEvent.bubbles = bubbles;
                                customEvent.cancelable = cancelable;
                                customEvent.view = view;
                                customEvent.detail = detail;

                                target.fireEvent("on" + type, customEvent);

                            } else {
                                throw Error("No event simulation framework present.");
                            }
                        }
                    }
                }
            }
        };

        var simulateGestureEvent = function(target, type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, scale, rotation) {
            var customEvent;

            if(!UA.ios || UA.ios < 2.0) {
                throw Error("Native gesture DOM eventframe is not available in this platform.");
            }

            if(!target) {
                throw Error("Invalid target.");
            }

            if(isString(type)) {
                type = type.toLowerCase();

                if(!GESTURE_EVENTS[type]) {
                    throw Error("Event type '" + type + "' not supported.");
                }
            } else {
                throw Error("Event type must be a string.");
            }

            if(!isBoolean(bubbles)) { bubbles = true; }
            if(!isBoolean(cancelable)) { cancelable = true; }
            if(!isObject(view)) { view = window; }
            if(!$.isNumeric(detail)) { detail = 2; }
            if(!$.isNumeric(screenX)) { screenX = 0; }
            if(!$.isNumeric(screenY)) { screenY = 0; }
            if(!$.isNumeric(clientX)) { clientX = 0; }
            if(!$.isNumeric(clientY)) { clientY = 0; }
            if(!isBoolean(ctrlKey)) { ctrlKey = false; }
            if(!isBoolean(altKey)) { altKey = false; }
            if(!isBoolean(shiftKey)) { shiftKey = false; }
            if(!isBoolean(metaKey)) { metaKey = false; }

            if(!$.isNumeric(scale)) { scale = 1.0; }
            if(!$.isNumeric(rotation)) { rotation = 0.0; }

            customEvent = document.createEvent("GestureEvent");

            customEvent.initGestureEvent(type, bubbles, cancelable, view, detail,
                screenX, screenY, clientX, clientY,
                ctrlKey, altKey, shiftKey, metaKey,
                target, scale, rotation);

            target.dispatchEvent(customEvent);
        };

        var simulateTouchEvent = function(target, type, bubbles, cancelable, view, detail, screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, touches, targetTouches, changedTouches, scale, rotation) {
            var customEvent;

            if(!target) {
                throw Error("Invalid target.");
            }

            if(isString(type)) {
                type = type.toLowerCase();

                if(!TOUCH_EVENTS[type]) {
                    throw Error("Event type '" + type + "' not supported.");
                }
            } else {
                throw Error("Event type must be a string.");
            }

            if(type === 'touchstart' || type === 'touchmove') {
                if(touches.length === 0) {
                    throw Error('No touch object in touches');
                }
            } else if(type === 'touchend') {
                if(changedTouches.length === 0) {
                    throw Error('No touch object in changedTouches');
                }
            }

            if(!isBoolean(bubbles)) { bubbles = true; }
            if(!isBoolean(cancelable)) {
                cancelable = (type !== "touchcancel");
            }
            if(!isObject(view)) { view = window; }
            if(!$.isNumeric(detail)) { detail = 1; }
            if(!$.isNumeric(screenX)) { screenX = 0; }
            if(!$.isNumeric(screenY)) { screenY = 0; }
            if(!$.isNumeric(clientX)) { clientX = 0; }
            if(!$.isNumeric(clientY)) { clientY = 0; }
            if(!isBoolean(ctrlKey)) { ctrlKey = false; }
            if(!isBoolean(altKey)) { altKey = false; }
            if(!isBoolean(shiftKey)) { shiftKey = false; }
            if(!isBoolean(metaKey)) { metaKey = false; }
            if(!$.isNumeric(scale)) { scale = 1.0; }
            if(!$.isNumeric(rotation)) { rotation = 0.0; }


            if($.isFunction(document.createEvent)) {
                if(UA.android && !UA.chrome) {
                    customEvent = document.createEvent("MouseEvents");
                    customEvent.initMouseEvent(type, bubbles, cancelable, view, detail,
                        screenX, screenY, clientX, clientY,
                        ctrlKey, altKey, shiftKey, metaKey,
                        0, target);

                    customEvent.touches = touches || [];
                    customEvent.targetTouches = targetTouches || [];
                    customEvent.changedTouches = changedTouches || [];
                } else if(UA.ios) {
                    if(UA.ios >= 2.0) {
                        customEvent = document.createEvent("TouchEvent");

                        var createTouchByOptions = function(options) {
                            return document.createTouch(
                                options.view || window,
                                options.target,
                                options.identifier || $.now(),
                                options.pageX || 0,
                                options.pageY || 0,
                                options.screenX || 0,
                                options.screenY || 0
                            );
                        };

                        var createTouchListByArray = function(array) {
                            var result = array || [];

                            $.each(result, function(i) {
                                result[i] = createTouchByOptions(this);
                            });
                            return document.createTouchList.apply(document, result);
                        };

                        touches = createTouchListByArray(touches);
                        targetTouches = createTouchListByArray(targetTouches);
                        changedTouches = createTouchListByArray(changedTouches);

                        customEvent.initTouchEvent(type, bubbles, cancelable, view, detail,
                            screenX, screenY, clientX, clientY,
                            ctrlKey, altKey, shiftKey, metaKey,
                            touches, targetTouches, changedTouches,
                            scale, rotation);

                    } else {
                        throw Error('No touch event simulation framework present for iOS, ' + UA.ios + '.');
                    }
                } else if(UA.chrome && ('ontouchstart' in window)) {
                    customEvent = document.createEvent("UIEvent");

                    customEvent.initEvent(type, bubbles, cancelable);

                    customEvent.view = view;
                    customEvent.detail = detail;
                    customEvent.screenX = screenX;
                    customEvent.screenY = screenY;
                    customEvent.clientX = clientX;
                    customEvent.clientY = clientY;
                    customEvent.ctrlKey = ctrlKey;
                    customEvent.altKey = altKey;
                    customEvent.metaKey = metaKey;
                    customEvent.shiftKey = shiftKey;
                    customEvent.target = target;
                    customEvent.touches = touches || [];
                    customEvent.targetTouches = targetTouches || [];
                    customEvent.changedTouches = changedTouches || [];
                } else {
                    throw Error('Not supported agent yet, ' + UA.userAgent);
                }

                target.dispatchEvent(customEvent);

            } else {
                throw Error('No event simulation framework present.');
            }
        };

        return function(target, type, options) {
            options = options || {};

            if(MOUSE_EVENTS[type] || MS_POINTER_EVENTS[type] || POINTER_EVENTS[type]) {
                simulateMouseEvent(target, type, options.bubbles,
                    options.cancelable, options.view, options.detail, options.screenX,
                    options.screenY, options.clientX, options.clientY, options.ctrlKey,
                    options.altKey, options.shiftKey, options.metaKey, options.button,
                    options.relatedTarget);
            } else if(KEY_EVENTS[type]) {
                simulateKeyEvent(target, type, options.bubbles,
                    options.cancelable, options.view, options.ctrlKey,
                    options.altKey, options.shiftKey, options.metaKey,
                    options.keyCode, options.charCode);
            } else if(UI_EVENTS[type]) {
                simulateUIEvent(target, type, options.bubbles,
                    options.cancelable, options.view, options.detail);
            } else if(TOUCH_EVENTS[type]) {
                if((window && ("ontouchstart" in window)) && !(UA.chrome && UA.chrome < 6)) {
                    simulateTouchEvent(target, type,
                        options.bubbles, options.cancelable, options.view, options.detail,
                        options.screenX, options.screenY, options.clientX, options.clientY,
                        options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
                        options.touches, options.targetTouches, options.changedTouches,
                        options.scale, options.rotation);
                } else {
                    throw Error("Event '" + type + "' can't be simulated. Use gesture-simulate module instead.");
                }

            } else if(UA.ios && UA.ios >= 2.0 && GESTURE_EVENTS[type]) {
                simulateGestureEvent(target, type,
                    options.bubbles, options.cancelable, options.view, options.detail,
                    options.screenX, options.screenY, options.clientX, options.clientY,
                    options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
                    options.scale, options.rotation);

            } else {
                throw Error("Event '" + type + "' can't be simulated.");
            }
        };
    })();

    var pointerEventsSupport = navigator.pointerEnabled,
        msPointerEventsSupport = !!window.MSPointerEvent && !pointerEventsSupport;

    var simulatePointerEvent = function($element, type, options) {
        options = $.extend({
            canBubble: true,
            cancelable: true,
            type: type
        }, options);

        var event = document.createEvent(msPointerEventsSupport ? "MSPointerEvent" : "pointerEvent");

        var args = [];
        $.each(["type", "canBubble", "cancelable", "view", "detail", "screenX", "screenY", "clientX", "clientY", "ctrlKey", "altKey",
            "shiftKey", "metaKey", "button", "relatedTarget", "offsetX", "offsetY", "width", "height", "pressure", "rotation", "tiltX",
            "tiltY", "pointerId", "pointerType", "hwTimestamp", "isPrimary"], function(i, name) {
            if(name in options) {
                args.push(options[name]);
            } else {
                args.push(event[name]);
            }
        });
        event.initPointerEvent.apply(event, args);

        $element[0].dispatchEvent(event);
    };


    var result = function($element) {

        $element = $($element);

        var _x,
            _y,
            _scrollTop,
            _scrollLeft,
            _clock;

        var createTouchMock = function(options) {
            // NOTE: All parameters are optional.
            options = $.extend({
                // values: window
                view: window,             // The window in which the touch occurred

                // values: DOMNode || document || window || etc.
                target: null,           // The EventTarget for the touch

                // Returns a value uniquely identifying this point of contact
                // with the touch surface. This value remains consistent for every
                // event involving this finger's (or stylus's) movement
                // on the surface until it is lifted off the surface.
                //
                // values: unique ID of the Touch object
                identifier: $.now(),       // The value for Touch.identifier

                // Coordinates of the touch point relative to the viewport,
                // including any scroll offset.
                pageX: null,            // The value for Touch.pageX
                pageY: null,            // The value for Touch.pageY

                // Coordinates coordinate of the touch point relative to the screen,
                // not including any scroll offset.
                screenX: null,          // The value for Touch.screenX
                screenY: null,          // The value for Touch.screenY

                // Coordinates of the touch point relative to the viewport,
                // not including any scroll offset.
                clientX: null,          // The value for Touch.clientX
                clientY: null,          // The value for Touch.clientY

                // Radiuses of the ellipse that most closely circumscribes the area
                // of contact with the screen. The value is in pixels of the same scale as screenX/screenY.
                radiusX: null,          // The value for Touch.radiusX.
                radiusY: null,          // The value for Touch.radiusY.

                // The rotation angle, in degrees, of the contact area ellipse defined by Touch.radiusX and Touch.radiusY.
                // values: [ 0 .. 90 ]
                rotationAngle: null,    // The value for Touch.rotationAngle.

                // The amount of pressure the user is applying to the touch surface for this
                // values: [ 0.0 .. 1.0 ] - no pressure and the maximum amount of pressure respectively.
                force: null
            }, options);

            return options;
        };

        var createTouchEventMock = function(options) {
            options = $.extend({
                type: "",

                // values: window
                view: window,             // The window in which the touch occurred

                // values: DOMNode || document || window || etc.
                target: null,           // The EventTarget for the touch

                // Returns a value uniquely identifying this point of contact
                // with the touch surface. This value remains consistent for every
                // event involving this finger's (or stylus's) movement
                // on the surface until it is lifted off the surface.
                //
                // values: unique ID of the Touch object
                identifier: null,       // The value for Touch.identifier

                // Coordinates of the touch point relative to the viewport,
                // including any scroll offset.
                pageX: null,            // The value for Touch.pageX
                pageY: null,            // The value for Touch.pageY

                // Coordinates coordinate of the touch point relative to the screen,
                // not including any scroll offset.
                screenX: null,          // The value for Touch.screenX
                screenY: null,          // The value for Touch.screenY

                // Coordinates of the touch point relative to the viewport,
                // not including any scroll offset.
                clientX: null,          // The value for Touch.clientX
                clientY: null,          // The value for Touch.clientY

                // Radiuses of the ellipse that most closely circumscribes the area
                // of contact with the screen. The value is in pixels of the same scale as screenX/screenY.
                radiusX: null,          // The value for Touch.radiusX.
                radiusY: null,          // The value for Touch.radiusY.

                // The rotation angle, in degrees, of the contact area ellipse defined by Touch.radiusX and Touch.radiusY.
                // values: [ 0 .. 90 ]
                rotationAngle: null,    // The value for Touch.rotationAngle.

                // The amount of pressure the user is applying to the touch surface for this
                // values: [ 0.0 .. 1.0 ] - no pressure and the maximum amount of pressure respectively.
                force: null,

                touches: [],
                targetTouches: [],
                changedTouches: [],

                scale: 1,
                rotation: 0,

                ctrlKey: false,
                altKey: false,
                shiftKey: false,
                metaKey: false,

                detail: 0
            }, options);

            return options;
        };

        var createMouseEventMock = function(options) {
            options = $.extend({
                type: null,         // click, mousedown, mouseup, mouseover, mousemove, mouseout.

                canBubble: true,    // whether or not the event can bubble. Sets the value of
                cancelable: true,   // whether or not the event's default action can be prevented. Sets the value of event.cancelable.

                view: window,         // the Event's AbstractView. You should pass the window object here. Sets the value of event.view.
                detail: 0,       // the Event's mouse click count. Sets the value of event.detail

                screenX: 0,          // the Event's screen x coordinate. Sets the value of event.screenX.
                screenY: 0,          // the Event's screen y coordinate. Sets the value of event.screenY.

                clientX: 0,          // the Event's client x coordinate. Sets the value of event.clientX.
                clientY: 0,          // whether or not control key was depressed during the Event. Sets the value of event.ctrlKey.

                ctrlKey: false,          // whether or not alt key was depressed during the Event. Sets the value of event.altKey.
                altKey: false,           // whether or not shift key was depressed during the Event. Sets the value of event.shiftKey.
                shiftKey: false,         // whether or not meta key was depressed during the Event. Sets the value of event.metaKey.
                metaKey: false,          // the Event's mouse event.button.
                button: 0,              // Indicates which mouse button caused the event: 0 || 1 || 2

                relatedTarget: null    // the Event's related EventTarget. Only used with some event types (e.g. mouseover and mouseout). In other cases, pass null.
            }, options);

            return options;
        };


        var originalEvent = function(options) {
            var event;

            if(/(click|mouse|wheel|pointer)/i.test(options.type)) {
                event = createMouseEventMock(options);
            }

            if(/(touch)/i.test(options.type)) {
                event = createTouchEventMock(options);
            }

            return event;
        };

        var eventMock = function(type, options) {
            options = $.extend({
                type: type
            }, options);

            if(type.indexOf("touch") > -1) {
                var touch = createTouchMock(options);

                options = $.extend({
                    touches: type === "touchend" ? [] : [touch],
                    changedTouches: [touch],
                    targetTouches: type === "touchend" ? [] : [touch]
                }, options);
            }

            if(type.indexOf("pointer") > -1) {
                options = $.extend({
                    pointerType: "mouse"
                }, options);
            }


            var event = $.extend($.Event(options.type), originalEvent(options), options);

            event[$.expando] = false;
            event = $.event.fix(event);

            return event;
        };

        var createEvent = function(type, args) {
            return eventMock(type, $.extend({
                pageX: _x,
                pageY: _y,
                timeStamp: _clock,
                type: type,
                which: 1,
                target: $element.get(0)
            }, args));
        };

        var triggerEvent = function(type, args) {
            var event = createEvent(type, args);
            $element.trigger(event);
            return event;
        };

        return {
            eventMock: eventMock,

            start: function() {
                _x = 0;
                _y = 0;
                _scrollTop = 0;
                _scrollLeft = 0;
                _clock = $.now();
                return this;
            },

            touchStart: function() {
                triggerEvent("touchstart");
                return this;
            },

            touchMove: function(deltaX, deltaY) {
                _x += deltaX || 0;
                _y += deltaY || 0;
                triggerEvent("touchmove");
                return this;
            },

            touchEnd: function() {
                triggerEvent("touchend");
                return this;
            },

            touchCancel: function() {
                triggerEvent("touchcancel");
                return this;
            },

            pointerDown: function() {
                var eventName = msPointerEventsSupport ? "MSPointerDown" : "pointerdown";
                try {
                    simulatePointerEvent($element, eventName, { clientX: _x, clientY: _y, pointerType: "mouse", pointerId: 1 });
                } catch(e) {
                    triggerEvent(eventName, { pointerType: "mouse" });
                }
                return this;
            },

            pointerMove: function(deltaX, deltaY) {
                var eventName = msPointerEventsSupport ? "MSPointerMove" : "pointermove";

                _x += deltaX || 0;
                _y += deltaY || 0;
                try {
                    simulatePointerEvent($element, eventName, { clientX: _x, clientY: _y, pointerType: "mouse", pointerId: 1 });
                } catch(e) {
                    triggerEvent(eventName, { pointerType: "mouse" });
                }
                return this;
            },

            pointerUp: function() {
                var eventName = msPointerEventsSupport ? "MSPointerUp" : "pointerup";
                try {
                    simulatePointerEvent($element, eventName, { clientX: _x, clientY: _y, pointerType: "mouse", pointerId: 1 });
                } catch(e) {
                    triggerEvent(eventName, { pointerType: "mouse" });
                }
                return this;
            },

            pointerCancel: function() {
                var eventName = msPointerEventsSupport ? "MSPointerCancel" : "pointercancel";
                try {
                    simulatePointerEvent($element, eventName, { clientX: _x, clientY: _y, pointerType: "mouse", pointerId: 1 });
                } catch(e) {
                    triggerEvent(eventName, { pointerType: "mouse" });
                }
                return this;
            },

            mouseDown: function() {
                triggerEvent("mousedown");
                return this;
            },

            mouseMove: function(deltaX, deltaY) {
                _x += deltaX || 0;
                _y += deltaY || 0;
                triggerEvent("mousemove");
                return this;
            },

            mouseUp: function() {
                triggerEvent("mouseup");
                return this;
            },

            mouseOver: function() {
                triggerEvent("mouseover");
                return this;
            },

            mouseOut: function() {
                triggerEvent("mouseout");
                return this;
            },

            mouseEnter: function() {
                triggerEvent("mouseenter");
                return this;
            },

            mouseLeave: function() {
                triggerEvent("mouseleave");
                return this;
            },

            down: function(x, y) {
                _x = x || _x;
                _y = y || _y;
                pointerEventsSupport || msPointerEventsSupport ? this.pointerDown() : this.touchStart();
                this.mouseDown();
                return this;
            },

            move: function(x, y) {
                if($.isArray(x)) {
                    this.move.apply(this, x);
                } else {
                    pointerEventsSupport || msPointerEventsSupport ? this.pointerMove(x, y) : this.touchMove(x, y);
                    this.mouseMove();
                }
                return this;
            },

            up: function() {
                pointerEventsSupport || msPointerEventsSupport ? this.pointerUp() : this.touchEnd();
                this.mouseUp();
                this.click(true);
                return this;
            },

            scroll: function(x, y) {
                _scrollLeft += x;
                _scrollTop += y;

                $element
                    .scrollLeft(_scrollLeft)
                    .scrollTop(_scrollTop);
                return this;
            },

            click: function(clickOnly) {
                if(!clickOnly) {
                    this.down();
                    this.up();
                } else {
                    triggerEvent("click");
                }
                return this;
            },

            wheel: function(d) {
                if(document["onwheel"] !== undefined) {
                    triggerEvent("wheel", {
                        deltaY: -d / 30
                    });
                } else {
                    triggerEvent("mousewheel", {
                        wheelDelta: d
                    });
                }

                triggerEvent("scroll");
                return this;
            },

            wait: function(ms) {
                _clock += ms;
                return this;
            }
        };
    };

    result.simulateEvent = simulateEvent;

    return result;

}));
